{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b71c4556-da01-4860-b4d8-a4981431bf31",
   "metadata": {},
   "source": [
    "# Strands Agents with AgentCore Memory (Long-term) using tools"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "580a2aa3-6a72-4c2f-bfea-9964822c4f24",
   "metadata": {},
   "source": [
    "## Overview\n",
    "This notebook demonstrates how to implement long-term memory capabilities for conversational AI agents using Strands and AgentCore Memory. You'll learn how to extract and consolidate important information from short-term interactions, enabling an agent to recall key details across multiple conversation sessions over time.\n",
    "\n",
    "## Tutorial Details\n",
    "**Use Case:** Culinary Assistant with Persistent Memory\n",
    "\n",
    "| Information         | Details                                                                          |\n",
    "|:--------------------|:---------------------------------------------------------------------------------|\n",
    "| Tutorial type       | Long term Conversational                                                         |\n",
    "| Agent type          | Culinary Assistant                                                               |\n",
    "| Agentic Framework   | Strands Agents                                                                   |\n",
    "| LLM model           | Anthropic Claude Sonnet 3.7                                                      |\n",
    "| Tutorial components | AgentCore 'User Preferences' Memory Extraction, Memory Tool for storing and retrieving Memory              |\n",
    "| Example complexity  | Beginner                                                                     |\n",
    "\n",
    "You'll learn to:\n",
    "- Configure AgentCore Memory with extraction strategies for long-term retention\n",
    "- Hydrate memory with previous conversation history\n",
    "- Use long-term memory to deliver personalized experiences across conversation sessions\n",
    "- Integrate Strands Agent Framework with the AgentCore Memory tool\n",
    "\n",
    "## Scenario Context\n",
    "\n",
    "In this tutorial, you'll step into the role of a Culinary Assistant designed to deliver highly personalized restaurant recommendations. By leveraging AgentCore Memory's long-term retention and automatic information extraction, the agent can remember user preferences—such as dietary choices and favorite cuisines—across multiple conversations. This persistent memory enables the agent to provide tailored suggestions and a seamless user experience, even as conversations span days or weeks. The scenario demonstrates how structured memory organization and configurable strategies empower conversational AI to move beyond short-term recall, creating truly engaging and context-aware interactions.\n",
    "\n",
    "\n",
    "## Architecture\n",
    "\n",
    "<div style=\"text-align:left\">\n",
    "    <img src=\"architecture.png\" width=\"65%\" />\n",
    "</div>\n",
    "\n",
    "\n",
    "## Prerequisites\n",
    "\n",
    "To execute this tutorial you will need:\n",
    "- Python 3.10+\n",
    "- AWS credentials with Amazon Bedrock AgentCore Memory permissions\n",
    "- Amazon Bedrock AgentCore SDK\n",
    "Let's get started by setting up our environment and creating our long-term memory resource with the appropriate extraction strategy!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e081f107-453e-4297-8b54-52f09dc9de9a",
   "metadata": {},
   "source": [
    "## Step 1: Environment set up\n",
    "Let's begin importing all the necessary libraries and defining the clients to make this notebook work."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27c1a741-4244-4e21-b383-1d4d1b306c3e",
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install -qr requirements.txt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d988b230-549f-44c8-8a9c-66bdda8e4505",
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "import logging\n",
    "import time\n",
    "from datetime import datetime"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b0236d0-6b5e-4dd9-96fc-c02709fcc6e3",
   "metadata": {},
   "source": [
    "Define the region and the role with the appropiate permissions for Amazon Bedrock models and AgentCore"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "33337482-9a1e-4d6c-8d2f-80e394d4f230",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s - %(levelname)s - %(message)s\", datefmt=\"%Y-%m-%d %H:%M:%S\")\n",
    "logger = logging.getLogger(\"culinary-memory\")\n",
    "\n",
    "region = os.getenv('AWS_REGION', 'us-west-2')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "54f57dad-9309-46fa-a0b6-f1dceadfb65b",
   "metadata": {},
   "source": [
    "## Step 2: Creating Memory with Long-Term Strategies\n",
    "\n",
    "In this section, we'll create a memory resource configured with long-term memory capabilities. Unlike our previous short-term memory example, this implementation includes specific memory strategies that enable consolidated information retention."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dc4cbd5d-ed80-4ed8-a214-161f568abf2a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from bedrock_agentcore.memory import MemoryClient\n",
    "from bedrock_agentcore.memory.constants import StrategyType\n",
    "\n",
    "client = MemoryClient(region_name=region)\n",
    "\n",
    "memory_name = \"CulinaryAssistant\"\n",
    "memory_id = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b30affac-ba24-4b66-9c30-d20f46b46c55",
   "metadata": {},
   "outputs": [],
   "source": [
    "from botocore.exceptions import ClientError\n",
    "\n",
    "try:\n",
    "    print(\"Creating Long-Term Memory...\")\n",
    "\n",
    "    # We use a more descriptive name for our long-term memory resource\n",
    "    memory_name = memory_name\n",
    "\n",
    "    # Create memory with user preference strategy\n",
    "    memory = client.create_memory_and_wait(\n",
    "        name=memory_name,\n",
    "        description=\"Culinary Assistant Agent with long term memory\",\n",
    "        strategies=[{\n",
    "                    StrategyType.USER_PREFERENCE.value: {\n",
    "                        \"name\": \"UserPreferences\",\n",
    "                        \"description\": \"Captures user preferences\",\n",
    "                        \"namespaces\": [\"user/{actorId}/preferences\"]\n",
    "                    }\n",
    "                }],\n",
    "        event_expiry_days=7,\n",
    "        max_wait=300,\n",
    "        poll_interval=10\n",
    "    )\n",
    "\n",
    "    memory_id = memory['memoryId']\n",
    "    print(f\"Memory created successfully with ID: {memory_id}\")\n",
    "    \n",
    "except ClientError as e:\n",
    "    if e.response['Error']['Code'] == 'ValidationException' and \"already exists\" in str(e):\n",
    "        # If memory already exists, retrieve its ID\n",
    "        memories = client.list_memories()\n",
    "        memory_id = next((m['id'] for m in memories if m['id'].startswith(memory_name)), None)\n",
    "        logger.info(f\"Memory already exists. Using existing memory ID: {memory_id}\")\n",
    "except Exception as e:\n",
    "    # Handle any errors during memory creation\n",
    "    logger.info(f\"❌ ERROR: {e}\")\n",
    "    import traceback\n",
    "    traceback.print_exc()\n",
    "    # Cleanup on error - delete the memory if it was partially created\n",
    "    if memory_id:\n",
    "        try:\n",
    "            client.delete_memory_and_wait(memory_id=memory_id)\n",
    "            logger.info(f\"Cleaned up memory: {memory_id}\")\n",
    "        except Exception as cleanup_error:\n",
    "            logger.info(f\"Failed to clean up memory: {cleanup_error}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d07c1923-ab99-48ac-a0cd-20e99ac19ae4",
   "metadata": {},
   "source": [
    "### Understanding Long-Term Memory Strategies\n",
    "\n",
    "The key difference in this memory creation is the addition of a **memory strategy**. Let's break down the components:\n",
    "\n",
    "#### 1. User Preference Memory Strategy\n",
    "\n",
    "This strategy automatically identifies and extracts user preferences from conversations:\n",
    "\n",
    "```python\n",
    "\"userPreferenceMemoryStrategy\": {\n",
    "    \"name\": \"UserPreferences\",\n",
    "    \"description\": \"Captures user preferences\",\n",
    "    \"namespaces\": [\"user/{actorId}/preferences\"]\n",
    "}\n",
    "```\n",
    "\n",
    "#### 2. Memory Namespaces\n",
    "\n",
    "The `namespaces` parameter defines where extracted information is stored:\n",
    "\n",
    "```python\n",
    "\"namespaces\": [\"user/{actorId}/preferences\"]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42caddab-067b-4149-bc89-49e326b63e6f",
   "metadata": {},
   "source": [
    "This memory strategy creates a more sophisticated memory system that doesn't just remember conversations, but actually understands and organizes the important information within those conversations for future use."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f1286ee9-1c6a-4dcd-a8fd-09a2c3b44327",
   "metadata": {},
   "source": [
    "## Step 3: Saving Previous Conversations to Memory\n",
    "\n",
    "In this section, we'll demonstrate how to hydrate the short-term memory, which automatically triggers the long-term memory extraction process behind the scenes.\n",
    "\n",
    "### Hydrating Short-Term Memory\n",
    "\n",
    "When we save conversations to a memory resource configured with extraction strategies, the system automatically processes this information for long-term retention without requiring additional code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c9acdc73-191f-4bd5-8ae4-049bd29cffb9",
   "metadata": {},
   "outputs": [],
   "source": [
    "actor_id = f\"user-{datetime.now().strftime('%Y%m%d%H%M%S')}\"\n",
    "session_id = f\"foodie-{datetime.now().strftime('%Y%m%d%H%M%S')}\"\n",
    "namespace = f\"user/{actor_id}/preferences\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ff244b8d-945e-4a9c-a9fb-076b85ef6340",
   "metadata": {},
   "outputs": [],
   "source": [
    "previous_messages = [\n",
    "    (\"Hi, I'm John\", \"USER\"),\n",
    "    (\"Hi John, how can I help you with food recommendations today?\", \"ASSISTANT\"),\n",
    "    (\"I'm looking for some vegetarian dishes to try this weekend.\", \"USER\"),\n",
    "    (\"That sounds great! I'd be happy to help with vegetarian recommendations. Do you have any specific ingredients or cuisine types you prefer?\", \"ASSISTANT\"),\n",
    "    (\"Yes, I really like tofu and fresh vegetables in my dishes\", \"USER\"),\n",
    "    (\"Perfect! Tofu and fresh vegetables make for excellent vegetarian meals. I can suggest some stir-fries, Buddha bowls, or tofu curries. Do you have any other preferences?\", \"ASSISTANT\"),\n",
    "    (\"I also really enjoy Italian cuisine. I love pasta dishes and would like them to be vegetarian-friendly.\", \"USER\"),\n",
    "    (\"Excellent! Italian cuisine has wonderful vegetarian options. I can recommend pasta primavera, mushroom risotto, eggplant parmesan, or penne arrabbiata. The combination of Italian flavors with vegetarian ingredients creates delicious meals!\", \"ASSISTANT\"),\n",
    "    (\"I spent 2 hours looking through cookbooks but couldn't find inspiring vegetarian Italian recipes\", \"USER\"),\n",
    "    (\"I'm sorry you had trouble finding inspiring recipes! Let me help you with some creative vegetarian Italian dishes. How about stuffed bell peppers with Italian herbs and rice, spinach and ricotta cannelloni, or a Mediterranean vegetable lasagna?\", \"ASSISTANT\"),\n",
    "    (\"Hey, I appreciate food assistants with good taste\", \"USER\"),\n",
    "    (\"Ha! I definitely try to bring good taste to the table! Speaking of which, shall we explore some more vegetarian Italian recipes that might inspire you?\", \"ASSISTANT\")\n",
    "]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3207c445-7da2-48b0-a5f7-7d1cfbf1d275",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"\\nHydrating short term memory with previous conversations...\")\n",
    "\n",
    "# Save the conversation history to short-term memory\n",
    "initial = client.create_event(\n",
    "    memory_id=memory_id,\n",
    "    actor_id=actor_id,\n",
    "    session_id=session_id,\n",
    "    messages=previous_messages,\n",
    ")\n",
    "print(\"✓ Conversation saved in short term memory\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3fc66c0-5f52-424b-a0c3-caebdee7614d",
   "metadata": {},
   "source": [
    "Let's make sure the event containing the conversation messages was stored correctly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9f78246-d518-49fd-af76-bb0c9c0add82",
   "metadata": {},
   "outputs": [],
   "source": [
    "events = client.list_events(\n",
    "    memory_id=memory_id,\n",
    "    actor_id=actor_id,\n",
    "    session_id=session_id,\n",
    "    max_results=5\n",
    ")\n",
    "events"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc7bdb56-bc81-4d16-b86a-a8485bb1643e",
   "metadata": {},
   "source": [
    "This cell configures the logging system to display informative messages during execution, helping us track what's happening as our code runs."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b8a5e4ea-cc67-46ac-957a-7a1c9cc2e71e",
   "metadata": {},
   "source": [
    "### What Happens Behind the Scenes\n",
    "\n",
    "After the `create_event` call, the following occurs automatically:\n",
    "\n",
    "1. **Short-Term Storage**: The complete conversation is saved in raw form\n",
    "2. **Extraction Trigger**: The memory system detects that this memory has the UserPreference strategy configured\n",
    "3. **Background Processing**: Without any additional code, the system:\n",
    "   - Analyzes the conversation for preference indicators\n",
    "   - Identifies statements like \"I'm vegetarian\" and \"I really enjoy Italian cuisine\"\n",
    "   - Extracts these preferences into structured data\n",
    "4. **Long-Term Consolidation**: The extracted preferences are saved in the configured namespace (`user/{actorId}/preferences`)\n",
    "\n",
    "Extraction and consolidation happen automatically - we only need to mantain a conversation with the agent or hydrate the short-term memory, and the strategies we configured during memory creation take care of the rest.\n",
    "\n",
    "This automatic process ensures that important information is preserved in long-term memory even after the short-term conversation records expire."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ef17bb40-66f8-406e-ac63-2da53d9c5468",
   "metadata": {},
   "source": [
    "\n",
    "## Retrieving Long-Term Memories\n",
    "\n",
    "In this section, we'll explore how to access the extracted preferences that have been stored in long-term memory. Unlike short-term memory retrieval which focuses on conversation turns, long-term memory retrieval focuses on accessing structured information that has been extracted and consolidated."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ab1ab05-a759-41a4-b558-8a0dcc3086cd",
   "metadata": {},
   "source": [
    "### Accessing User Preferences from Long-Term Memory\n",
    "\n",
    "To retrieve information from long-term memory, we use the namespace structure defined during memory creation:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4b3b1f3f-cd32-418e-ad04-af8736907e83",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Adding a 30s wait to ensure the memory extraction has time to process the event\n",
    "time.sleep(30)\n",
    "\n",
    "try:\n",
    "    # Query the memory system for food preferences\n",
    "    food_preferences = client.retrieve_memories(\n",
    "        memory_id=memory_id,\n",
    "        namespace=namespace,\n",
    "        query=\"food preferences\",\n",
    "        top_k=3  # Return up to 3 most relevant results\n",
    "    )\n",
    "\n",
    "    if food_preferences:\n",
    "        print(f\"Retrieved {len(food_preferences)} relevant preference records:\")\n",
    "        for i, record in enumerate(food_preferences):\n",
    "            print(f\"\\nMemory {i+1}:\")\n",
    "            print(f\"- Content: {record.get('content', 'Not specified')}\")\n",
    "    else:\n",
    "        print(\"No matching preference records found.\")\n",
    "\n",
    "except Exception as e:\n",
    "    print(f\"Error retrieving preference records: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6402f50-318f-4c1c-94d5-6dedf3494022",
   "metadata": {},
   "source": [
    "This method enables the retrieval of relevant memories when needed. Now we learned the basics let's build up our agent!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7fad557-0390-4027-b45e-0de74fd92f50",
   "metadata": {},
   "source": [
    "## Step 4: Creating the agent \n",
    "In this section, we'll explore how to integrate AgentCore Memory with a Strands Agent using the native `agent_core_memory` tool.\n",
    "\n",
    "#### Setting Up the Agent with Long term Memory Capabilities\n",
    "To create a memory-enabled agent, we'll use the Strands framework and connect it to our AgentCore Memory resource"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1ace0e24-6856-48f2-8038-4bade9858937",
   "metadata": {},
   "outputs": [],
   "source": [
    "from strands import tool, Agent\n",
    "from strands_tools.agent_core_memory import AgentCoreMemoryToolProvider"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "195c3775-cce3-4b47-8d9e-db1d7e8dcae8",
   "metadata": {},
   "outputs": [],
   "source": [
    "system_prompt = f\"\"\"You are the Culinary Assistant, a sophisticated restaurant recommendation assistant.\n",
    "\n",
    "PURPOSE:\n",
    "- Help users discover restaurants based on their preferences\n",
    "- Remember user preferences throughout the conversation\n",
    "- Provide personalized dining recommendations\n",
    "\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "da9a8ec9-547e-466a-bdc8-0e68f46b02d2",
   "metadata": {},
   "outputs": [],
   "source": [
    "provider = AgentCoreMemoryToolProvider(\n",
    "    memory_id=memory_id,\n",
    "    actor_id=actor_id,\n",
    "    session_id=session_id,\n",
    "    namespace=namespace\n",
    ")\n",
    "\n",
    "agent = Agent(tools=provider.tools, system_prompt=system_prompt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "984cbf6c-ddfa-494a-b0c2-f03947b39e22",
   "metadata": {},
   "source": [
    "As we have already populated our short term and long term memory, let's directly retrieve the memory from the agent!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "60c57280-7072-4890-9a98-944708906cda",
   "metadata": {},
   "outputs": [],
   "source": [
    "agent(\"Give me restaurant recommendations in Irvine based on my food preferences\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "410a16bf-f99d-4f46-92b1-7ed7da8feebf",
   "metadata": {},
   "source": [
    "The agent should have used the retrieve_memory_records method to retrieve the user's memories."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c43fcdc-a8f6-4da7-8dd9-d34a1a619237",
   "metadata": {},
   "source": [
    "Great! You know have a working Strands Agent capable of retrieving memories from the AgentCore Long Term Memory!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3105d43e-0795-42c6-8b1c-e6425983de57",
   "metadata": {},
   "source": [
    "## Clean up\n",
    "Let's delete the memory to clean up the resources used in this notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af61a83e-2128-4a79-a9cc-49dbee2d70c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "#client.delete_memory_and_wait(\n",
    "#        memory_id = memory_id,\n",
    "#        max_wait = 300,\n",
    "#        poll_interval =10\n",
    "#)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "conda_python3",
   "language": "python",
   "name": "conda_python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
